Notebook analyzuje zastoupení žen v PS ČR na základě dat z archivu PS ČR. Součástí analýzy je extrapolace historických dat pomocí lineární regrese.
Nastavení notebooku pro prostředí Google Colab a pro lokální běh.
# Specifické příkazy pro prostředí Google Colab
if 'google.colab' in str(get_ipython()):
import os, sys
os.chdir('/content')
# Stažení knihovny
! ls parlamentikon || git clone "https://github.com/parlamentikon/parlamentikon.git" --branch main
os.chdir('/content/parlamentikon/notebooks')
instalace_zavislosti = True
if instalace_zavislosti:
! pip install -r ../requirements.txt 1>/dev/null
instalace_knihovny = False
if instalace_knihovny:
! pip install .. 1>/dev/null
else:
# Přidání cesty pro lokální import knihovny
import sys, os
sys.path.insert(0, os.path.abspath('..'))
from datetime import datetime, timedelta
import plotly.graph_objects as go
import pandas as pd
import numpy as np
from parlamentikon.Hlasovani import Organy
from parlamentikon.PoslanciOsoby import Poslanci
# Data se budou pokaždé znovu stahovat z achivu PS
stahni=True
vsechny_organy = Organy(volebni_obdobi=-1)
snemovny = vsechny_organy[vsechny_organy.nazev_typ_organ_cz == 'Parlament'].od_organ.dt.year.sort_values()
poslanci = {}
for i in snemovny:
poslanci[i] = Poslanci(volebni_obdobi=i, stahni=stahni)
2021-08-27:03:09:08 INFO [utility.py:21] Stahuji 'https://www.psp.cz/eknih/cdrom/opendata/poslanci.zip'. 2021-08-27:03:09:10 INFO [utility.py:21] Stahuji 'https://www.psp.cz/eknih/cdrom/opendata/poslanci.zip'. 2021-08-27:03:09:11 WARNING [Snemovna.py:149] While merging 'funkce' with 'typ_funkce': Dropping ['nazev_typ_organ_en__typ_funkce', 'typ_organ_obecny__typ_funkce', 'id_typ_organ__typ_funkce', 'typ_id_typ_organ__typ_funkce', 'nazev_typ_organ_cz__typ_funkce'] because of abundance. 2021-08-27:03:09:12 INFO [utility.py:21] Stahuji 'https://www.psp.cz/eknih/cdrom/opendata/poslanci.zip'. 2021-08-27:03:09:14 WARNING [Snemovna.py:149] While merging 'funkce' with 'typ_funkce': Dropping ['nazev_typ_organ_en__typ_funkce', 'typ_organ_obecny__typ_funkce', 'id_typ_organ__typ_funkce', 'typ_id_typ_organ__typ_funkce', 'nazev_typ_organ_cz__typ_funkce'] because of abundance. 2021-08-27:03:09:14 INFO [utility.py:21] Stahuji 'https://www.psp.cz/eknih/cdrom/opendata/poslanci.zip'. 2021-08-27:03:09:16 WARNING [Snemovna.py:149] While merging 'funkce' with 'typ_funkce': Dropping ['nazev_typ_organ_en__typ_funkce', 'typ_organ_obecny__typ_funkce', 'id_typ_organ__typ_funkce', 'typ_id_typ_organ__typ_funkce', 'nazev_typ_organ_cz__typ_funkce'] because of abundance. 2021-08-27:03:09:17 INFO [utility.py:21] Stahuji 'https://www.psp.cz/eknih/cdrom/opendata/poslanci.zip'. 2021-08-27:03:09:18 WARNING [Snemovna.py:149] While merging 'funkce' with 'typ_funkce': Dropping ['nazev_typ_organ_en__typ_funkce', 'typ_organ_obecny__typ_funkce', 'id_typ_organ__typ_funkce', 'typ_id_typ_organ__typ_funkce', 'nazev_typ_organ_cz__typ_funkce'] because of abundance. 2021-08-27:03:09:19 INFO [utility.py:21] Stahuji 'https://www.psp.cz/eknih/cdrom/opendata/poslanci.zip'. 2021-08-27:03:09:20 WARNING [Snemovna.py:149] While merging 'funkce' with 'typ_funkce': Dropping ['nazev_typ_organ_en__typ_funkce', 'typ_organ_obecny__typ_funkce', 'id_typ_organ__typ_funkce', 'typ_id_typ_organ__typ_funkce', 'nazev_typ_organ_cz__typ_funkce'] because of abundance. 2021-08-27:03:09:21 INFO [utility.py:21] Stahuji 'https://www.psp.cz/eknih/cdrom/opendata/poslanci.zip'. 2021-08-27:03:09:22 WARNING [Snemovna.py:149] While merging 'funkce' with 'typ_funkce': Dropping ['nazev_typ_organ_en__typ_funkce', 'typ_organ_obecny__typ_funkce', 'id_typ_organ__typ_funkce', 'typ_id_typ_organ__typ_funkce', 'nazev_typ_organ_cz__typ_funkce'] because of abundance. 2021-08-27:03:09:23 INFO [utility.py:21] Stahuji 'https://www.psp.cz/eknih/cdrom/opendata/poslanci.zip'. 2021-08-27:03:09:24 WARNING [Snemovna.py:149] While merging 'funkce' with 'typ_funkce': Dropping ['nazev_typ_organ_en__typ_funkce', 'typ_organ_obecny__typ_funkce', 'id_typ_organ__typ_funkce', 'typ_id_typ_organ__typ_funkce', 'nazev_typ_organ_cz__typ_funkce'] because of abundance. 2021-08-27:03:09:25 INFO [utility.py:21] Stahuji 'https://www.psp.cz/eknih/cdrom/opendata/poslanci.zip'. 2021-08-27:03:09:27 WARNING [Snemovna.py:149] While merging 'funkce' with 'typ_funkce': Dropping ['nazev_typ_organ_en__typ_funkce', 'typ_organ_obecny__typ_funkce', 'id_typ_organ__typ_funkce', 'typ_id_typ_organ__typ_funkce', 'nazev_typ_organ_cz__typ_funkce'] because of abundance.
def pohlavi_dle_snemovny(snemovna, dalsi_snemovna=None):
x = poslanci[snemovna]
# Najdi datum konce aktuální sněmovny.
last_do_parlament = vsechny_organy[vsechny_organy.id_organ == x.id_organ.iloc[0]].do_organ.iloc[0]
# Poslanci nemohou mít mandát platný déle než do konce aktuální sněmovny.
x.do_parlament.mask(pd.to_datetime(x.do_parlament) > last_do_parlament, last_do_parlament, inplace=True)
if dalsi_snemovna != None:
# Najdi datum začátku další sněmovny.
next_od_parlament = vsechny_organy[vsechny_organy.id_organ == poslanci[dalsi_snemovna].id_organ.iloc[0]].od_organ.iloc[0]
# Poslanci nemohou mít platný mandát po začátku další sněmovny.
x.do_parlament.mask(pd.to_datetime(x.do_parlament) > next_od_parlament, next_od_parlament, inplace=True)
#print(snemovna, dalsi_snemovna, x.do_parlament.sort_values().to_list()[-1], next_od_parlament, x[x.do_parlament > next_od_parlament])
# Pro dny, ve kterých se mění složení poslanecké sněmovny, sečteme tyto změny vzhledem k pohlaví.
# Používáme nezávislé sčítače pro příchody a odchody.
zeny_od = x[x.pohlavi == 'žena'].groupby(x.od_parlament.dt.date, dropna=False).size().to_frame('zeny_od').rename_axis(index={'od_parlament': 'datum'})
zeny_do = x[x.pohlavi == 'žena'].groupby(x.do_parlament.dt.date, dropna=False).size().to_frame('zeny_do').rename_axis(index={'do_parlament': 'datum'})
muzi_od = x[x.pohlavi == 'muž'].groupby(x.od_parlament.dt.date, dropna=False).size().to_frame('muzi_od').rename_axis(index={'od_parlament': 'datum'})
muzi_do = x[x.pohlavi == 'muž'].groupby(x.do_parlament.dt.date, dropna=False).size().to_frame('muzi_do').rename_axis(index={'do_parlament': 'datum'})
## Datum 'do_parlament' může být první den, kdy již poslanci nejsou ve sněmovně. Ale také poslední den platnosti mandátu.
## V devadesátých letech se nejspíš konec platnosti mandátu zaznamenával s odlišnou sémantikou než v současnosti.
## Jelikož se jedná o malé časové úseky, prozatím tuto nesrovnalost ignorujeme
# zeny_do.index = (pd.to_datetime(zeny_do.index) + pd.DateOffset(days=1)).date
# zeny_do.index.name = 'datum'
# muzi_do.index = (pd.to_datetime(muzi_do.index) + pd.DateOffset(days=1)).date
# muzi_do.index.name = 'datum'
# Spojíme sčítače příchodů a odchodů ze Sněmovny do jednoho frameu.
df = zeny_od.merge(muzi_od, how='outer', on='datum').merge(muzi_do, how='outer', on='datum').merge(zeny_do, how='outer', on='datum').sort_index()
# +R9dk0 hodnoty doplníme nulami.
df = df.fillna(0)
# Fix pandas error which converted groupby sizes from int to float
df['zeny_od'], df['zeny_do'] = df['zeny_od'].astype(int), df['zeny_do'].astype(int)
df['muzi_od'], df['muzi_do'] = df['muzi_od'].astype(int), df['muzi_do'].astype(int)
# Od sčítačů příchodů do sněmovny odečteme scítače odchodů.
# Tím dostaneme pro každé "zlomové datum" bilanci počtu žen a mužů.
df['zeny_cnt'] = df['zeny_od'].cumsum() - df['zeny_do'].cumsum()
df['muzi_cnt'] = df['muzi_od'].cumsum() - df['muzi_do'].cumsum()
# Hodnoty by se měly posčítat na 200 (počet poslanců).
# Vzhledem k nejasné interpretaci 'do_parlament' to v několika málo případech není pravda, ale jen na krátkou dobu.
df['check_cnt'] = df['zeny_cnt'] + df['muzi_cnt']
# Pri rozpusteni snemovny se atribut 'do_parlament' pro vsechny poslance nastavuje na urcite datum.
# Sčítač dle pohlaví pak nabývá hodnoty 0.
# Pro účely vizualizace je vhodné data doplnit ještě jeden bod s počty poslanců těsně před koncem sněmovny.
if (len(df) > 1) and (df.iloc[-1].check_cnt == 0) and not(pd.isna(df.index[-1])):
last_zeny_cnt, last_muzi_cnt = df.iloc[-2].zeny_cnt, df.iloc[-2].muzi_cnt
before_snemovna_do = (df.index[-1] - pd.DateOffset(days=1)).date()
df.loc[before_snemovna_do, 'zeny_cnt'] = last_zeny_cnt
df.loc[before_snemovna_do, 'muzi_cnt'] = last_muzi_cnt
df.loc[before_snemovna_do, 'check_cnt'] = last_zeny_cnt + last_muzi_cnt
df = df.sort_index()
# Pro poslance v současné sněmovně zpravidla platí: do_parlament == nan.
# Index s nan hodnotou prepíšeme současným datem.
if (len(df) > 1) and (pd.isna(df.index[-1])):
index_as_list = df.index.tolist()
index_as_list[-1] = pd.to_datetime("now").tz_localize('Europe/Prague').date()
df.index = index_as_list
#df.rename(index={np.NaT: pd.to_datetime("now").tz_localize('Europe/Prague')}, inplace=True)
df.iloc[-1, df.columns.get_loc('zeny_cnt')] = df.iloc[-2].zeny_cnt
df.iloc[-1, df.columns.get_loc('muzi_cnt')] = df.iloc[-2].muzi_cnt
df['check_cnt'] = df['zeny_cnt'] + df['muzi_cnt']
df.drop(df[df.check_cnt == 0].index, inplace = True)
# Spočítejme nyní poměry ...
df['zeny_pct'] = 100 * df['zeny_cnt'] / (df['zeny_cnt'] + df['muzi_cnt'])
df['muzi_pct'] = 100 * df['muzi_cnt'] / (df['zeny_cnt'] + df['muzi_cnt'])
# Nastavme sněmovnu (na konstantní hodnotu).
df['snemovna'] = snemovna
df = df.sort_index()
df = df[['zeny_pct', 'muzi_pct', 'zeny_cnt', 'muzi_cnt', 'check_cnt', 'snemovna']]
return df
zastoupeni = []
l_snemovny = list(snemovny)
for idx, snemovna in enumerate(l_snemovny):
if idx < len(l_snemovny) - 1:
dalsi_snemovna = l_snemovny[idx + 1]
else:
dalsi_snemovna = None
zastoupeni.append(pohlavi_dle_snemovny(snemovna, dalsi_snemovna))
zastoupeni_df = pd.concat(zastoupeni, join='outer')
zastoupeni_df.index = pd.to_datetime(zastoupeni_df.index)
zastoupeni_df.index.name = 'datum'
zastoupeni_df.head()
| zeny_pct | muzi_pct | zeny_cnt | muzi_cnt | check_cnt | snemovna | |
|---|---|---|---|---|---|---|
| datum | ||||||
| 1992-06-06 | 10.000000 | 90.000000 | 20.0 | 180.0 | 200.0 | 1992 |
| 1993-11-09 | 9.547739 | 90.452261 | 19.0 | 180.0 | 199.0 | 1992 |
| 1993-11-11 | 9.500000 | 90.500000 | 19.0 | 181.0 | 200.0 | 1992 |
| 1995-05-23 | 9.547739 | 90.452261 | 19.0 | 180.0 | 199.0 | 1992 |
| 1995-05-25 | 9.500000 | 90.500000 | 19.0 | 181.0 | 200.0 | 1992 |
# Kontrola smyslupnosti dat ... check_cnt by neměl být víc než 200.
# Bylo by dobré ověřit, že hodnoty menší než 200 jsou pouze krátkodobé fluktuace, ke kterým dochází během výměny poslanců v PS.
zastoupeni_df[(zastoupeni_df.check_cnt != 200)]
| zeny_pct | muzi_pct | zeny_cnt | muzi_cnt | check_cnt | snemovna | |
|---|---|---|---|---|---|---|
| datum | ||||||
| 1993-11-09 | 9.547739 | 90.452261 | 19.0 | 180.0 | 199.0 | 1992 |
| 1995-05-23 | 9.547739 | 90.452261 | 19.0 | 180.0 | 199.0 | 1992 |
| 1996-04-29 | 9.547739 | 90.452261 | 19.0 | 180.0 | 199.0 | 1992 |
| 1996-05-06 | 9.547739 | 90.452261 | 19.0 | 180.0 | 199.0 | 1992 |
| 1996-11-01 | 15.075377 | 84.924623 | 30.0 | 169.0 | 199.0 | 1996 |
| 1996-12-17 | 14.646465 | 85.353535 | 29.0 | 169.0 | 198.0 | 1996 |
| 1997-01-16 | 14.572864 | 85.427136 | 29.0 | 170.0 | 199.0 | 1996 |
| 1997-06-02 | 14.572864 | 85.427136 | 29.0 | 170.0 | 199.0 | 1996 |
| 1997-06-11 | 14.572864 | 85.427136 | 29.0 | 170.0 | 199.0 | 1996 |
| 1997-08-27 | 14.572864 | 85.427136 | 29.0 | 170.0 | 199.0 | 1996 |
| 1997-08-29 | 14.572864 | 85.427136 | 29.0 | 170.0 | 199.0 | 1996 |
| 1997-11-11 | 14.572864 | 85.427136 | 29.0 | 170.0 | 199.0 | 1996 |
| 1997-12-01 | 14.572864 | 85.427136 | 29.0 | 170.0 | 199.0 | 1996 |
| 1997-12-02 | 15.075377 | 84.924623 | 30.0 | 169.0 | 199.0 | 1996 |
| 1998-01-26 | 15.075377 | 84.924623 | 30.0 | 169.0 | 199.0 | 1996 |
| 1998-02-12 | 15.075377 | 84.924623 | 30.0 | 169.0 | 199.0 | 1996 |
| 1998-02-25 | 15.075377 | 84.924623 | 30.0 | 169.0 | 199.0 | 1996 |
| 2001-02-28 | 15.656566 | 84.343434 | 31.0 | 167.0 | 198.0 | 1998 |
K extrapolaci zastoupení žen v PS do budoucnosti použijeme lineární regresi.
from sklearn import linear_model
zastoupeni_df[pd.isna(zastoupeni_df.index)]
zastoupeni_df['days_from_start'] = (zastoupeni_df.index - zastoupeni_df.index[0]).days
zastoupeni_df.head()
| zeny_pct | muzi_pct | zeny_cnt | muzi_cnt | check_cnt | snemovna | days_from_start | |
|---|---|---|---|---|---|---|---|
| datum | |||||||
| 1992-06-06 | 10.000000 | 90.000000 | 20.0 | 180.0 | 200.0 | 1992 | 0 |
| 1993-11-09 | 9.547739 | 90.452261 | 19.0 | 180.0 | 199.0 | 1992 | 521 |
| 1993-11-11 | 9.500000 | 90.500000 | 19.0 | 181.0 | 200.0 | 1992 | 523 |
| 1995-05-23 | 9.547739 | 90.452261 | 19.0 | 180.0 | 199.0 | 1992 | 1081 |
| 1995-05-25 | 9.500000 | 90.500000 | 19.0 | 181.0 | 200.0 | 1992 | 1083 |
x = zastoupeni_df['days_from_start'].values.reshape(-1, 1)
y = zastoupeni_df['zeny_pct'].values
model = linear_model.LinearRegression().fit(x, y)
linear_model.LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)
x_historical = zastoupeni_df['days_from_start'].values[:-2]
x_extrapolated = np.linspace(zastoupeni_df['days_from_start'].max(), 34593, 50, endpoint=True).round(decimals=0)
x_values = np.concatenate((x_historical, x_extrapolated))
x_vector = [[v] for v in x_values]
y_pred = model.predict(x_vector)
zastoupeni_extrapolace_df = pd.DataFrame(
{"zeny_pct_extrapolace": y_pred},
index=[zastoupeni_df.index[0] + pd.DateOffset(days=num_days) for num_days in x_values]
)
zastoupeni_extrapolace_df.index.name = 'datum'
zastoupeni_extrapolace_df.head()
| zeny_pct_extrapolace | |
|---|---|
| datum | |
| 1992-06-06 | 11.522600 |
| 1993-11-09 | 12.102295 |
| 1993-11-11 | 12.104520 |
| 1995-05-23 | 12.725384 |
| 1995-05-25 | 12.727609 |
fig = go.Figure()
for snemovna in snemovny:
df = pohlavi_dle_snemovny(snemovna)
hovertemplate_zeny = f"Sněmovna: {snemovna}" +'<br>Datum: %{x|%d. %m. %Y}<br>Pohlaví: ženy<br>Počet: %{y}<extra></extra>'
hovertemplate_muzi = f"Sněmovna: {snemovna}" +'<br>Datum: %{x|%d. %m. %Y}<br>Pohlaví: muži<br>Počet: %{y}<extra></extra>'
# Ženy
fig.add_trace(go.Scatter(
x = df.index,
y = df.zeny_pct,
mode = 'markers+lines',
hoverinfo = 'text',
hovertemplate=hovertemplate_zeny,
name=f"ženy ({snemovna})",
stackgroup=snemovna
))
# Muži
fig.add_trace(go.Scatter(
x = df.index,
y = df.muzi_pct,
name = f"muži ({snemovna})",
hoverinfo = 'text',
hovertemplate=hovertemplate_muzi,
mode = 'markers+lines',
#line = dict(shape='linear'),
stackgroup=snemovna
))
fig.add_trace(go.Scatter(
x = zastoupeni_extrapolace_df.index,
y = zastoupeni_extrapolace_df.zeny_pct_extrapolace.round(2),
name = f"ženy (extrapolace)",
mode = 'lines',
line = dict(color='green', width=2, dash='dash')
))
fig.add_shape(type="line",
x0=zastoupeni_extrapolace_df.index[0],
y0=50,
x1=zastoupeni_extrapolace_df.index[-1],
y1=50,
line=dict(color="black", width=1, dash='dot')
)
middle_x = zastoupeni_extrapolace_df.index[0] + (zastoupeni_extrapolace_df.index[-1] - zastoupeni_extrapolace_df.index[0]) / 2
fig.add_annotation(
x=middle_x,
y=50,
text="50% zastoupení",
arrowhead=2,
)
layout = go.Layout(
title="Zastoupení žen v Poslanecké sněmovně ČR",
#plot_bgcolor="#FFFFFF",
hovermode="x",
xaxis=dict(title="Datum"),
yaxis=dict(
title="Zastoupení žen",
tickvals = [0, 20, 40, 60, 80, 100],
ticktext = ["0%", "20%", "40%", "60%", "80%", "100%"]
)
)
fig.update_layout(layout)
fig.update_layout(showlegend=False, autosize=False,
width=1000,
height=500,)
fig.show()
# Data bez extrapolace
! mkdir -p "../docs/data"
export_path = "../docs/data/zastoupeni_zen_v_PS_CR.csv"
zastoupeni_df[['zeny_pct', 'muzi_pct', 'zeny_cnt', 'muzi_cnt', 'snemovna']].to_csv(export_path)
! ls -l {export_path}
-rw-r--r-- 1 runner docker 5703 Aug 27 03:09 ../docs/data/zastoupeni_zen_v_PS_CR.csv
# Data s extrapolací
m = zastoupeni_df.merge(zastoupeni_extrapolace_df, how='outer', on='datum')
m = m.sort_index()
m.head()
! mkdir -p "../docs/data"
export_path = "../docs/data/extrapolace_zastoupeni_zen_v_PS_CR.csv"
m[['zeny_pct', 'muzi_pct', 'zeny_cnt', 'muzi_cnt', 'snemovna', 'zeny_pct_extrapolace']].to_csv(export_path)
! ls -l {export_path}
-rw-r--r-- 1 runner docker 10280 Aug 27 03:09 ../docs/data/extrapolace_zastoupeni_zen_v_PS_CR.csv
print(f"Poslední běh notebooku: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}.")
Poslední běh notebooku: 27.08.2021 03:09:29.